{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RNN学习笔记\n",
    "\n",
    "RNN是两种神经网络模型的缩写，一种是递归神经网络（Recursive Neural Network），一种是循环神经网络（Recurrent Neural Network）。虽然这两种神经网络有着千丝万缕的联系，但是本文主要讨论的是第二种神经网络模型——循环神经网络（Recurrent Neural Network）。\n",
    "\n",
    "循环神经网络是指一个随着时间的推移，重复发生的结构。在自然语言处理（NLP），语音图像等多个领域均有非常广泛的应用。RNN网络和其他网络最大的不同就在于RNN能够实现某种“记忆功能”，是进行时间序列分析时最好的选择。如同人类能够凭借自己过往的记忆更好地认识这个世界一样。RNN也实现了类似于人脑的这一机制，对所处理过的信息留存有一定的记忆，而不像其他类型的神经网络并不能对处理过的信息留存记忆。\n",
    "\n",
    "## RNN原理\n",
    "\n",
    "循环神经网络的原理并不十分复杂，本节主要从原理上分析RNN的结构和功能，不涉及RNN的数学推导和证明，整个网络只有简单的输入输出和网络状态参数。一个典型的RNN神经网络如图所示：\n",
    "\n",
    "> <img src=\"http://static.open-open.com/lib/uploadImg/20150829/20150829181722_204.png\" width=\"200\"/>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由上图可以看出：一个典型的RNN网络包含一个输入x，一个输出h和一个神经网络单元A。和普通的神经网络不同的是，RNN网络的神经网络单元A不仅仅与输入和输出存在联系，其与自身也存在一个回路。这种网络结构就揭示了RNN的实质：上一个时刻的网络状态信息将会作用于下一个时刻的网络状态。如果上图的网络结构仍不够清晰，RNN网络还能够以时间序列展开成如下形式：\n",
    "\n",
    "> ![rnn-2.png](http://static.open-open.com/lib/uploadImg/20150829/20150829181722_413.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "等号右边是RNN的展开形式。由于RNN一般用来处理序列信息，因此下文说明时都以时间序列来举例，解释。等号右边的等价RNN网络中最初始的输入是x0，输出是h0，这代表着0时刻RNN网络的输入为x0，输出为h0，网络神经元在0时刻的状态保存在A中。当下一个时刻1到来时，此时网络神经元的状态不仅仅由1时刻的输入x1决定，也由0时刻的神经元状态决定。以后的情况都以此类推，直到时间序列的末尾t时刻。\n",
    "\n",
    "上面的过程可以用一个简单的例子来论证：假设现在有一句话“I want to play basketball”，由于自然语言本身就是一个时间序列，较早的语言会与较后的语言存在某种联系，例如刚才的句子中“play”这个动词意味着后面一定会有一个名词，而这个名词具体是什么可能需要更遥远的语境来决定，因此一句话也可以作为RNN的输入。回到刚才的那句话，这句话中的5个单词是以时序出现的，我们现在将这五个单词编码后依次输入到RNN中。首先是单词“I”，它作为时序上第一个出现的单词被用作x0输入，拥有一个h0输出，并且改变了初始神经元A的状态。单词“want”作为时序上第二个出现的单词作为x1输入，这时RNN的输出和神经元状态将不仅仅由x1决定，也将由上一时刻的神经元状态或者说上一时刻的输入x0决定。之后的情况以此类推，直到上述句子输入到最后一个单词“basketball”。\n",
    "\n",
    "接下来我们需要关注RNN的神经元结构：\n",
    "\n",
    "> <img src=\"http://static.open-open.com/lib/uploadImg/20150829/20150829181722_450.png\" style=\"height:184px\"/>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上图依然是一个RNN神经网络的时序展开模型，中间t时刻的网络模型揭示了RNN的结构。可以看到，原始的RNN网络的内部结构非常简单。神经元A在t时刻的状态仅仅是t-1时刻神经元状态与t时刻网络输入的双曲正切函数的值，这个值不仅仅作为该时刻网络的输出，也作为该时刻网络的状态被传入到下一个时刻的网络状态中，这个过程叫做RNN的正向传播（forward propagation）。注：双曲正切函数的解析式如下：\n",
    "\n",
    "$$f(x)=tanh(x)=\\frac{sinh(x)}{cosh(x)}=\\frac {e^{x}-e^{-x}}{e^{x}+e^{-x}}$$\n",
    "\n",
    "双曲正切函数的求导如下：\n",
    "\n",
    "$$f'(x)=(tanh(x))^{'}=\\frac{1}{cosh^{2}(x)}=\\frac {4}{(e^{x}+e^{-x})^{2}}=1-tanh^{2}(x)$$\n",
    "\n",
    "双曲正切函数的图像如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from matplotlib import pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "x = np.arange(-10, 10, 0.1)\n",
    "y = np.tanh(x)\n",
    "fig = plt.figure(\"tanh-fig\")\n",
    "ax = fig.add_subplot(111)\n",
    "ax.plot(x, y, \"r\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里就带来一个问题：为什么RNN网络的激活函数要选用双曲正切而不是sigmod呢？（RNN的激活函数除了双曲正切，RELU函数也用的非常多）原因在于RNN网络在求解时涉及时间序列上的大量求导运算，使用sigmod函数容易出现梯度消失，且sigmod的导数形式较为复杂。事实上，即使使用双曲正切函数，传统的RNN网络依然存在梯度消失问题，无法“记忆”长时间序列上的信息，这个bug直到LSTM上引入了单元状态后才算较好地解决。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数学基础\n",
    "\n",
    "这一节主要介绍与RNN相关的数学推导，由于RNN是一个时序模型，因此其求解过程可能和一般的神经网络不太相同。首先需要介绍一下RNN完整的结构图，上一节给出的RNN结构图省去了很多内部参数，仅仅作为一个概念模型给出。\n",
    "\n",
    "> ![RNN_STRUCTURE](https://pic1.zhimg.com/80/v2-3bc30e9a2d740fff417e47dcf7d636bc_hd.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上图表明了RNN网络的完整拓扑结构，从图中我们可以看到RNN网络中的参数情况。在这里我们只分析t时刻网络的行为与数学推导。t时刻网络迎来一个输入xt，网络此时刻的神经元状态st用如下式子表达：\n",
    "\n",
    "$$s_{t}=\\phi (Ux_{t}+Ws_{t-1})$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "t时刻的网络状态st不仅仅要输入到下一个时刻t+1的网络状态中去，还要作为该时刻的网络输出。当然，st不能直接输出，在输出之前还要再乘上一个系数V，而且为了误差逆传播时的方便通常还要对输出进行归一化处理，也就是对输出进行softmax化。因此，t时刻网络的输出ot表达为如下形式：\n",
    "\n",
    "$$o_{t}=\\varphi (Vs_{t})$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了表达方便，笔者将上述两个公式做如下变换：\n",
    "\n",
    "$$\\left\\{\n",
    "\\begin{aligned}\n",
    "&s_{t}^{*}=Ux_{t}+Ws_{t-1}\\rightarrow s_{t}=\\phi (s_{t}^{*})\\\\\n",
    "&o_{t}^{*}=Vs_{t}\\rightarrow o_{t}=\\varphi (o_{t}^{*})\n",
    "\\end{aligned}\n",
    "\\right.$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以上，就是RNN网络的数学表达了，接下来我们需要求解这个模型。在论述具体解法之前首先需要明确两个问题：\n",
    "\n",
    "* 优化目标函数是什么？\n",
    "\n",
    "* 待优化的量是什么？\n",
    "\n",
    "只有在明确了这两个问题之后才能对模型进行具体的推导和求解。关于第一个问题，笔者选取模型的损失函数作为优化目标；关于第二个问题，我们从RNN的结构图中不难发现：只要我们得到了模型的U，V，W这三个参数就能完全确定模型的状态。因此该优化问题的优化变量就是RNN的这三个参数。顺便说一句，RNN模型的U，V，W三个参数是全局共享的，也就是说不同时刻的模型参数是完全一致的，这个特性使RNN得参数变得稍微少了一些。\n",
    "\n",
    "### 损失函数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "不做过多的讨论，RNN的损失函数选用交叉熵（Cross Entropy），这是机器学习中使用最广泛的损失函数之一了，其通常的表达式如下所示：\n",
    "\n",
    "$$Loss=-\\sum_{i=0}^{n}y_{i}lny_{i}^{*}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面式子是交叉熵的标量形式，y_i是真实的标签值，y_i\\*是模型给出的预测值，最外面之所以有一个累加符号是因为模型输出的一般都是一个多维的向量，只有把n维损失都加和才能得到真实的损失值。交叉熵在应用于RNN时需要做一些改变：\n",
    "\n",
    "* 首先，RNN的输出是向量形式，没有必要将所有维度都加在一起，直接把损失值用向量表达就可以了；\n",
    "\n",
    "* 其次，由于RNN模型处理的是序列问题，因此其模型损失不能只是一个时刻的损失，应该包含全部N个时刻的损失。\n",
    "\n",
    "故RNN模型在t时刻的损失函数写成如下形式：\n",
    "\n",
    "$$Loss_{t}=-[y_{t}ln(o_{t})+(y_{t}-1)ln(1-o_{t})]$$\n",
    "\n",
    "全部N个时刻的损失函数（全局损失）表达为如下形式：\n",
    "\n",
    "$$Loss=\\sum_{t=1}^{N}Loss_{t}=-\\sum_{t=1}^{N}[y_{t}ln(o_{t})+(y_{t}-1)ln(1-o_{t})]$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "需要说明的是：yt是t时刻输入的真实标签值，ot为模型的预测值，N代表全部N个时刻。下文中为了书写方便，将Loss简记为L。在结束本小节之前，最后补充一个softmax函数的求导公式：\n",
    "\n",
    "$$\\varphi '(x)=\\varphi (x)(1-\\varphi (x))$$\n",
    "\n",
    "### BPTT算法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于RNN模型与时间序列有关，因此不能直接使用BP（back propagation）算法。针对RNN问题的特殊情况，提出了BPTT算法。BPTT的全称是“随时间变化的反向传播算法”（back propagation through time）。这个方法的基础仍然是常规的链式求导法则，接下来开始具体推导。虽然RNN的全局损失是与全部N个时刻有关的，但为了简单笔者在推导时只关注t时刻的损失函数。\n",
    "\n",
    "首先求出t时刻下损失函数关于o_t\\*的微分：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial o_{t}^{*}}=\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\frac{\\partial o_{t}}{\\partial o_{t}^{*}}=\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\frac{\\varphi (o_{t}^{*})}{\\partial o_{t}^{*}}=\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "求出损失函数关于参数V的微分：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial V}=\\frac{\\partial L_{t}}{\\partial Vs_{t}}\\times \\frac{\\partial Vs_{t}}{\\partial V}=\n",
    "\\frac{\\partial L_{t}}{\\partial o_{t}^{*}}\\times s_{t}=\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})\\times s_{t}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因此，全局损失关于参数V的微分为：\n",
    "\n",
    "$$\\frac{\\partial L}{\\partial V}=\\sum_{t=1}^{N}\\frac{\\partial L_{t}}{\\partial V}=\\sum_{t=1}^{N}\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})\\times s_{t}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "求出t时刻的损失函数关于关于st\\*的微分：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial s_{t}^{*}}=\\frac{\\partial s_{t}}{\\partial s_{t}^{*}}\\times \\frac{\\partial s_{t}^{T}V^{T}}{\\partial s_{t}}\\times \\frac{\\partial L_{t}}{\\partial Vs_{t}}=\\phi'(s_{t}^{*})\\times V^{T} \\times \\frac{\\partial L_{t}}{\\partial o_{t}^{*}}=\\phi'(s_{t}^{*})\\times V^{T} \\times \\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "求出t时刻的损失函数关于s_t-1\\*的微分：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial s_{t-1}^{*}}=\\frac{\\partial s_{t}^{*}}{\\partial s_{t-1}^{*}}\\times \\frac{\\partial L_{t}}{\\partial s_{t}^{*}}=\\frac{\\partial [\\phi (s_{t-1}^{*})W+Ux_{t}]}{\\partial s_{t-1}^{*}}\\times \\frac{\\partial L_{t}}{\\partial s_{t}^{*}}=W\\phi'(s_{t-1}^{*}) \\times \\frac{\\partial L_{t}}{\\partial s_{t}^{*}}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "求出t时刻损失函数关于参数U的偏微分。注意：由于是时间序列模型，因此t时刻关于U的微分与前t-1个时刻都有关，在具体计算时可以限定最远回溯到前n个时刻，但在推导时需要将前t-1个时刻全部带入：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial U}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial s_{k}^{*}}{\\partial U}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial (Ux_{k}+Ws_{k-1})}{\\partial U}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times x_{k}^{T}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因此，全局损失关于U的偏微分为：\n",
    "\n",
    "$$\\frac{\\partial L}{\\partial U}=\\sum_{t=1}^{N}\\frac{\\partial L_{t}}{\\partial U}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial s_{k}^{*}}{\\partial U}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times x_{k}^{T}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "求t时刻损失函数关于参数W的偏微分，和上面相同的道理，在这里仍然要计算全部前t-1时刻的情况：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial W}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial s_{k}^{*}}{\\partial W}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial (Ux_{k}+Ws_{k-1})}{\\partial W}=\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times s_{k-1}^{T}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因此，全局损失关于参数W的微分结果为：\n",
    "\n",
    "$$\\frac{\\partial L}{\\partial W}=\\sum_{t=1}^{N}\\frac{\\partial L_{t}}{\\partial W}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times \\frac{\\partial s_{k}^{*}}{\\partial W}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times s_{k-1}^{T}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "至此，全局损失函数关于三个主要参数的微分都已经得到了。整理如下：\n",
    "\n",
    "$$\\left\\{\n",
    "\\begin{aligned}\n",
    "&\\frac{\\partial L}{\\partial V}=\\sum_{t=1}^{N}\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})\\times s_{t}\\\\\n",
    "&\\frac{\\partial L}{\\partial U}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times x_{k}^{T}\\\\\n",
    "&\\frac{\\partial L}{\\partial W}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times s_{k-1}^{T}\n",
    "\\end{aligned}\n",
    "\\right.$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来进一步化简上述微分表达式，化简的主要方向为t时刻的损失函数关于ot的微分以及关于st\\*的微分。已知t时刻损失函数的表达式，求关于ot的微分：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial o_{t}}=\\frac {-\\partial [y_{t}ln(o_{t})+(y_{t}-1)ln(1-o_{t})]}{\\partial o_{t}}=- (\\frac{y_{t}}{o_{t}}+\\frac{y_{t}-o_{t}}{1-o_{t}})=-\\frac{y_t-o_{t}}{o_{t}(1-o_{t})}$$\n",
    "\n",
    "softmax函数求导：\n",
    "\n",
    "$$\\phi '(o_{t}^{*})=o_{t}(1-o_{t})$$\n",
    "\n",
    "因此\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})=-\\frac{y_t-o_{t}}{o_{t}(1-o_{t})} \\times o_{t}(1-o_{t})=o_{t}-y_{t}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "又因为：\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial s_{t}^{*}}=\\phi'(s_{t}^{*})\\times V^{T} \\times \\frac{\\partial L_{t}}{\\partial o_{t}}\\times \\varphi' (o_{t}^{*})=(1-\\phi ^{2}(s_{t}^{*}))\\times [V^{T} \\times (o_{t}-y_{t})]=(1-s_{t}^{2})\\times [V^{T} \\times (o_{t}-y_{t})]$$\n",
    "\n",
    "且\n",
    "\n",
    "$$\\frac{\\partial L_{t}}{\\partial s_{t-1}^{*}}=W\\phi'(s_{t-1}^{*}) \\times \\frac{\\partial L_{t}}{\\partial s_{t}^{*}}=(1-s_{t-1}^{2})\\times W^{T}\\times\\frac{\\partial L_{t}}{\\partial s_{t}^{*}}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "有了上面的数学推导，我们可以得到全局损失关于U，V，W三个参数的梯度公式：\n",
    "\n",
    "$$\\left\\{\n",
    "\\begin{aligned}\n",
    "&\\frac{\\partial L}{\\partial V}=\\sum_{t=1}^{N}(o_{t}-y_{t})\\times s_{t}\\\\\n",
    "&\\frac{\\partial L}{\\partial U}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times x_{k}^{T}\\\\\n",
    "&\\frac{\\partial L}{\\partial W}=\\sum_{t=1}^{N}\\sum_{k=1}^{t}\\frac{\\partial L_{t}}{\\partial s_{k}^{*}}\\times s_{k-1}^{T}\n",
    "\\end{aligned}\n",
    "\\right.$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于参数U和W的微分公式不仅仅与t时刻有关，还与前面的t-1个时刻都有关，因此无法写出直接的计算公式。不过上面已经给出了t时刻的损失函数关于s_t-1的微分递推公式，想来求解这个式子也是十分简单的，在这里就不赘述了。\n",
    "\n",
    "以上就是关于BPTT算法的全部数学推导。从最终结果可以看出三个公式的偏微分结果非常简单，在具体的优化过程中可以直接带入进行计算。对于这种优化问题来说，最常用的方法就是梯度下降法。针对本文涉及的RNN问题，可以构造出三个参数的梯度更新公式：\n",
    "\n",
    "$$\\left\\{\n",
    "\\begin{aligned}\n",
    "&V:=V-\\eta \\times \\frac{\\partial L}{\\partial V}\\\\\n",
    "&U:=U-\\eta \\times \\frac{\\partial L}{\\partial U}\\\\\n",
    "&W:=W -\\eta \\times \\frac{\\partial L}{\\partial W}\n",
    "\\end{aligned}\n",
    "\\right.$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "依靠上述梯度更新公式就能够迭代求解三个参数，直到三个参数的值发生收敛。\n",
    "\n",
    "## 后记\n",
    "\n",
    "这是笔者第一次尝试推导RNN的数学模型，在推导过程中遇到了非常多的bug。非常感谢互联网上的一些公开资料和博客，给了我非常大的帮助和指引。接下来笔者将尝试实现一个单隐层的RNN模型用于实现一个语义预测模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
